import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;

public class ObjectStorage
{
	private class NeuronCache
	{
		// NOTE: we DO NEED this for loading files containing CADU(s),
		// because in large CADUs, Neurons might have been created
		// long before their input fields and -nodes were added,
		// so that GetDrawObjectByCompoundId() would do too
		// many loop iterations to find a Neuron!

		private HashMap<Long, Neuron> NeuronCache = new HashMap<Long, Neuron>();

		private void AddNeuron(Neuron Neuron)
		{
			// TODO: loading a file does sometimes not work properly if this is
			// enabled, don't know why... there should be only one Neuron with
			// this CompoundId... test with Test.LooksChaotic.ncide
			// ->
			// if (NeuronCache.get(Neuron.GetCompoundId()) != null)
			// return;

			NeuronCache.put(Neuron.GetCompoundId(), Neuron);
		}

		private Neuron GetNeuronByCompoundId(long CompoundId)
		{
			return NeuronCache.get(CompoundId);
		}
	}

	private long FileTypeMagicNumber = 0x529FE8A528F41C38L; // just typed this randomly
	private int FileTypeVersion = 2; // increase if changing file format

	private ArrayList<DrawObject> DrawObjects = new ArrayList<DrawObject>(); // durably contains (references to) ALL objects (like Neurons, Nodes, Lines etc.)

	private ArrayList<String> HormonesAlreadyUpdated = new ArrayList<String>(); // temporary, to speed up the decision if a Hormone needs to be RE-updated

	public DrawObject AddObject(String DrawObjectTypeDescription, Object Start, Object End)
	{
		DrawObject AddedMainObject = null;

		// verify
		if (Start == null)
		{
			System.out.println("error in ObjectStorage.AddObject(): Start object is null!");
			return AddedMainObject;
		}
		if (End == null && DrawObjectTypeDescription.equals("Line"))
		{
			System.out.println("error in ObjectStorage.AddObject(): End object is null!");
			return AddedMainObject;
		}

		if (!(Tools.CheckHeapSize()))
		{
			System.out.println("error in ObjectStorage.AddObject(): not enough heap memory left!");
			return AddedMainObject;
		}

		// begin
		String DrawObjectTypeDescriptionAdapted;

		if (DrawObjectTypeDescription.equals("LearningNeuron"))
			DrawObjectTypeDescriptionAdapted = "Neuron"; // every LearningNeuron is actually a normal Neuron with a special, internal flag set
		else
			DrawObjectTypeDescriptionAdapted = DrawObjectTypeDescription;

		switch (DrawObjectTypeDescriptionAdapted)
		{
		case "CommentField":
			CommentField CommentFieldNew = new CommentField((Point) Start, new Point(400, 400));
			DrawObjects.add(CommentFieldNew);
			ArrayList<DrawObject> CommentFieldNewChildObjects = CommentFieldNew.GenerateChildObjects();
			for (int m = 0; m < CommentFieldNewChildObjects.size(); m++)
				DrawObjects.add(CommentFieldNewChildObjects.get(m));
			AddedMainObject = CommentFieldNew;
			break;
		case "TextField":
			TextField TextFieldNew = new TextField((Point) Start);
			DrawObjects.add(TextFieldNew);
			AddedMainObject = TextFieldNew;
			break;
		case "Line":
			if (End instanceof Neuron) // End is Object, cannot use IsNeuron()
			{
				Neuron.NeuronInputDrawObjects InputFieldDrawObjects = ((Neuron) End).AddInputFieldAndNode();
				DrawObjects.add(InputFieldDrawObjects.GetField());
				DrawObjects.add(InputFieldDrawObjects.GetNode());
				Line LineNew = new Line((DrawObject) Start, (DrawObject) InputFieldDrawObjects.GetNode());
				DrawObjects.add(LineNew);
				AddedMainObject = LineNew;
			}
			else
			{
				Line LineNew = new Line((DrawObject) Start, (DrawObject) End);
				DrawObjects.add(LineNew);
				AddedMainObject = LineNew;
			}
			break;
		case "Neuron":
			Neuron NeuronNew = new Neuron((Point) Start);
			DrawObjects.add(NeuronNew);

			if (DrawObjectTypeDescription.equals("LearningNeuron"))
				NeuronNew.SetIsLearningNeuron(true);

			NeuronOutputNode NeuronOutputNode = NeuronNew.AddOutputNode();
			DrawObjects.add(NeuronOutputNode);

			// NeuronInputNodes/NeuronInputFields added dynamically (see above at 'case "Line":')
			// *if user draws a Line onto Neuron*

			AddedMainObject = NeuronNew;
			break;
		case "NeuronInputField":
			NeuronInputField NeuronInputFieldNew = new NeuronInputField((Point) Start);
			DrawObjects.add(NeuronInputFieldNew);
			AddedMainObject = NeuronInputFieldNew;
			break;
		case "Node":
			Node NodeNew = new Node((Point) Start);
			DrawObjects.add(NodeNew);
			AddedMainObject = NodeNew;
			break;
		case "SampleAndHold":
			SampleAndHold SampleAndHoldNew = new SampleAndHold((Point) Start);
			DrawObjects.add(SampleAndHoldNew);
			SampleAndHoldInputNode SampleAndHoldInputNode = SampleAndHoldNew.AddInputNode();
			DrawObjects.add(SampleAndHoldInputNode);
			SampleAndHoldOutputNode SampleAndHoldOutputNode = SampleAndHoldNew.AddOutputNode();
			DrawObjects.add(SampleAndHoldOutputNode);
			AddedMainObject = SampleAndHoldNew;
			break;
		case "VisualField":
			VisualField VisualFieldNew = new VisualField((Point) Start);
			DrawObjects.add(VisualFieldNew);
			ArrayList<VisualFieldPixel> VisualFieldPixels = VisualFieldNew.AddVisualFieldPixels();
			for (int m = 0; m < VisualFieldPixels.size(); m++)
				DrawObjects.add(VisualFieldPixels.get(m));
			AddedMainObject = VisualFieldNew;
			break;
		}

		return AddedMainObject;
	}

	public void AddObjectDirectly(DrawObject Object)
	{
		if (!(Object.IsZombie())) // not marked as to be disposed?
		{
			DrawObjects.add(Object);
		}
	}

	public void AfterCollectingAllExcitement()
	{
		for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
		{
			DrawObject o = i.next();
			o.AfterCollectingAllExcitementForNextTick();
		}
	}

	public void DeleteHormoneForAll(String HormoneName)
	{
		for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
		{
			DrawObject o = i.next();
			if (o.IsNeuron())
			{
				((Neuron) o).DeleteHormone(HormoneName);
			}
		}
	}

	public void DeleteObject(DrawObject ObjectToDelete)
	{
		// ATTENTION: do not call this for many objects to delete at once,
		// randomly removing objects from ArrayList<> is not fast,
		// instead call DeleteObjects()!

		ArrayList<DrawObject> ChildDrawObjects = ObjectToDelete.GetChildDrawObjects();

		// ***************************************************************
		// ATTENTION: does not work recursively (yet)! We assume there are
		// not further ChildDrawObjects within any ChildDrawObject!
		// ***************************************************************

		for (int m = 0; m < ChildDrawObjects.size(); m++)
		{
			ChildDrawObjects.get(m).MarkAsZombie();
			DrawObjects.remove(ChildDrawObjects.get(m));
		}

		ObjectToDelete.MarkAsZombie();

		DrawObjects.remove(ObjectToDelete);

		if (ChildDrawObjects != null)
		{
			ChildDrawObjects.clear(); // make sure gc throws away old ArrayList<DrawObject>
			ChildDrawObjects.trimToSize(); // make sure gc throws away old ArrayList<DrawObject>
			ChildDrawObjects = null; // make sure gc throws away old ArrayList<DrawObject>
		}
	}

	public void DeleteObjects(ArrayList<DrawObject> ObjectsToDelete)
	{
		int ZombieCount = 0;

		for (int i = 0; i < ObjectsToDelete.size(); i++)
		{
			DrawObject ObjectToDelete = ObjectsToDelete.get(i);

			ObjectToDelete.MarkAsZombie();
			ZombieCount++;

			ArrayList<DrawObject> ChildDrawObjects = ObjectToDelete.GetChildDrawObjects();

			// ***************************************************************
			// ATTENTION: does not work recursively (yet)! We assume there are
			// not further ChildDrawObjects within any ChildDrawObject!
			// ***************************************************************

			for (int m = 0; m < ChildDrawObjects.size(); m++)
			{
				ChildDrawObjects.get(m).MarkAsZombie();
				ZombieCount++;
			}
		}

		ArrayList<DrawObject> DrawObjectsNew = new ArrayList<DrawObject>(Math.max(0, DrawObjects.size() - ZombieCount)); // Math.max() IS required (if ObjectsToDelete contains an invalid high count of objects)

		for (int i = 0; i < DrawObjects.size(); i++)
		{
			if (!(DrawObjects.get(i).IsZombie()))
			{
				DrawObjectsNew.add(DrawObjects.get(i));
			}
		}

		// access the (kind of) "global" array...

		if (DrawObjects != null)
		{
			DrawObjects.clear(); // make sure gc throws away old ArrayList<DrawObject>
			DrawObjects.trimToSize(); // make sure gc throws away old ArrayList<DrawObject>
			DrawObjects = null; // make sure gc throws away old ArrayList<DrawObject>
		}

		DrawObjects = DrawObjectsNew;
	}

	public void DeleteUnnecessaryObjects()
	{
		for (int ReDo = 0; ReDo < Integer.MAX_VALUE; ReDo++)
		{
			ArrayList<DrawObject> DrawObjectsNew = new ArrayList<DrawObject>();

			for (int i = 0; i < DrawObjects.size(); i++)
			{
				DrawObject o = DrawObjects.get(i);

				if (!(o.IsZombie()))
				{
					if (o.IsLine())
					{
						DrawObject StartDrawObject = ((Line) o).GetStartDrawObject();
						DrawObject EndDrawObject = ((Line) o).GetEndDrawObject();

						// NOTE: we assume that if the reference StartDrawObject
						// or EndDrawObject still points to a DrawObject whose
						// reference was removed from DrawObjects, this DrawObject
						// is still present in memory (because of StartDrawObject
						// or EndDrawObject). Therefore we must use the IsZombie
						// flag to mark any DrawObject as being deleted.
						//
						// Better don't set StartDrawObject and EndDrawObject to
						// null or the following would probably not work any more:

						if (!(StartDrawObject.IsZombie()) && !(EndDrawObject.IsZombie()))
						{
							DrawObjectsNew.add(o);
						}
					}
					else
					{
						DrawObjectsNew.add(o);
					}
				}
			}

			if (DrawObjectsNew.size() < DrawObjects.size())
			{
				if (DrawObjects != null)
				{
					DrawObjects.clear(); // make sure gc throws away old ArrayList<DrawObject>
					DrawObjects.trimToSize(); // make sure gc throws away old ArrayList<DrawObject>
					DrawObjects = null; // make sure gc throws away old ArrayList<DrawObject>
				}

				DrawObjects = DrawObjectsNew;
			}
			else
			{
				break;
			}

			if (ReDo > 1000)
			{
				System.out.println("error in ObjectStorage.DeleteUnnecessaryObjects(): there seems to be an endless loop, debug this!");
				break;
			}
		}
	}

	public void Draw(Graphics g, boolean UseHandsomeDrawOrder)
	{
		if (UseHandsomeDrawOrder)
		{
			// draw e.g. VisualFields and its pixels and TextFields at last to appear on top, to cover Lines
			for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
			{
				DrawObject o = i.next();

				if (!(o.IsReducedDrawOrderEnabled()) || (o.GetProperties() & DrawObject.PROPERTY_DONT_DRAW_IN_REDUCED_DRAWING_MODE) == 0L)
				{
					if (o.IsLine())
					{
						o.Draw(g);
					}
				}
			}
			for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
			{
				DrawObject o = i.next();
				if (!(o.IsReducedDrawOrderEnabled()) || (o.GetProperties() & DrawObject.PROPERTY_DONT_DRAW_IN_REDUCED_DRAWING_MODE) == 0L)
				{
					if (!(o.IsLine()))
					{
						o.Draw(g);
					}
				}
			}
		}
		else
		{
			// use raw draw order (as objects were created/added)
			for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
			{
				DrawObject o = i.next();
				if (!(o.IsReducedDrawOrderEnabled()) || (o.GetProperties() & DrawObject.PROPERTY_DONT_DRAW_IN_REDUCED_DRAWING_MODE) == 0L)
				{
					o.Draw(g);
				}
			}
		}
	}

	public void ExciteLines()
	{
		for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
		{
			DrawObject o = i.next();
			if (o.IsLine())
			{
				DrawObject S = ((Line) o).GetStartDrawObject();
				// if (S.IsThereNoteworthyExcitement()) // excites connected Line also when afterglowing
				if (S.DoesActivate(S.GetExcitementMaxFromExcitementHistory())) // excites connected Lines only when being fully excited ("maximal yellow")
				{
					o.AddExcitementForNextTick(1.0);

					DrawObject E = ((Line) o).GetEndDrawObject();
					if (E.CanBeExcitedByUser()) // don't excite everything or problems e.g. with VisualFieldPixels, which "excite back" other objects via Lines for other VisualField patterns (tested)
					{
						E.AddExcitementForNextTick(1.0);
					}
				}
			}
		}
	}

	public void ExciteMultipleObjects(ArrayList<DrawObject> ObjectsToExcite)
	{
		for (Iterator<DrawObject> i = ObjectsToExcite.iterator(); i.hasNext();)
		{
			DrawObject o = i.next();
			o.AddExcitementForNextTick(1.0);
		}
	}

	public void ExciteTextFields(String TextFieldText)
	{
		// NOTE: there might be MULTIPLE TextFields with the same text!
		// This is intended to allow "connecting" distance SubSystems without
		// using forever many Lines.

		ArrayList<TextField> TextFields = GetTextFieldsByText(TextFieldText);

		for (int i = 0; i < TextFields.size(); i++)
		{
			TextField TextField = TextFields.get(i);

			if (TextField.GetText().equals(TextFieldText)) // just to make sure, Objects.GetTextFieldsByText() should have done this already
			{
				TextField.AddExcitementForNextTick(1.0);

				// set text of a TextField to e.g. "Adrenaline=50%" to "release" the
				// Hormone Adrenaline if being activated - recall any TextField can be
				// activated through Line(s) which end in them, so a programmatic
				// release of Hormones is possible!

				UpdateHormoneStrengthForAll(Neuron.ExtractHormoneName(TextField.GetText()), Neuron.ExtractHormoneStrength(TextField.GetText()));
			}
		}
	}

	public boolean ExistsLineFromTo(DrawObject O1, DrawObject O2)
	{
		for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
		{
			DrawObject o = i.next();
			if (o.IsLine())
			{
				if (((Line) o).GetStartDrawObject() == O1 && ((Line) o).GetEndDrawObject() == O2)
					return true;
				if (((Line) o).GetStartDrawObject() == O2 && ((Line) o).GetEndDrawObject() == O1)
					return true;
			}
		}

		return false;
	}

	public ArrayList<DrawObject> GetActivatedObjects()
	{
		ArrayList<DrawObject> ActivatedObjects = new ArrayList<DrawObject>();

		for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
		{
			DrawObject o = i.next();

			// "if (o.IsActivated()) ..." does not work, because ExcitementCurrent is
			// increased and decreased (loss) in the same method, so that ExcitementCurrent
			// does NEVER (durably) reach 1.0 and thus o.IsActivated() is always false here...
			// Think about how to fix this in a "logical" way...

			if (o.DoesActivate(o.GetExcitementMaxFromExcitementHistory()))
			// HACK (excitement history was intended for displaying purposes only, REPURPOSE (and rename) it?)
			{
				ActivatedObjects.add(o);
			}
		}

		return ActivatedObjects;
	}

	public ArrayList<DrawObject> GetActivatedObjects(DrawObject SampleDrawObjectOfDesiredClassType)
	{
		ArrayList<DrawObject> ActivatedObjects = new ArrayList<DrawObject>();

		for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
		{
			DrawObject o = i.next();

			if (o.DoesActivate(o.GetExcitementMaxFromExcitementHistory()))
			// HACK (excitement history was intended for displaying purposes only, REPURPOSE (and rename) it?)
			{
				if (o.getClass().isInstance(SampleDrawObjectOfDesiredClassType))
				{
					ActivatedObjects.add(o);
				}
			}
		}

		return ActivatedObjects;
	}

	public ArrayList<TextField> GetActivatedTextFields()
	{
		ArrayList<TextField> ActivatedTextFields = new ArrayList<TextField>();

		for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
		{
			DrawObject o = i.next();
			if (o.IsTextField())
			{
				if (o.IsActivated())
				{
					ActivatedTextFields.add((TextField) o);
				}
			}
		}

		return ActivatedTextFields;
	}

	public DrawObject GetDrawObjectByCompoundId(DrawObject SampleDrawObjectOfDesiredClassType, long CompoundId, ArrayList<DrawObject> DrawObjects)
	{
		// IMPORTANT: this function is mainly called when loading an .ncide file.
		// The purpose is to find e.g. a Neuron already added belonging to any
		// child object of that Neuron. As the Neuron was added usually shortly
		// before its child objects, we should search "in the surrounding" of
		// the child object. When loading an .ncide file, the child object
		// is always the last one in (the passed) DrawObjects.
		// So we should quickly find the parent (e.g. the Neuron) RIGHT BEFORE
		// the last element in DrawObjects. If this assumption does not apply
		// (because this function is used by an other caller), the reverse order
		// doesn't hurt, we'll find the desired object anyway.

		for (int i = DrawObjects.size() - 1; i >= 0; i--)
		{
			DrawObject o = DrawObjects.get(i);
			if (o.GetCompoundId() == CompoundId)
			{
				if (o.getClass().isInstance(SampleDrawObjectOfDesiredClassType))
				{
					return o;
				}
			}
		}

		return null;
	}

	public int GetDrawObjectCount()
	{
		return DrawObjects.size();
	}

	public ArrayList<DrawObject> GetMultiSelectionObjects()
	{
		ArrayList<DrawObject> MultiSelectionObjects = new ArrayList<DrawObject>();

		for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
		{
			DrawObject o = i.next();
			if (o.IsPartOfMultiSelection())
				MultiSelectionObjects.add(o);
		}

		return MultiSelectionObjects;
	}

	public Rectangle GetMultiSelectionRectangle(ArrayList<DrawObject> MultiSelectionObjects)
	{
		int xMin = Integer.MAX_VALUE;
		int yMin = Integer.MAX_VALUE;
		int xMax = Integer.MIN_VALUE;
		int yMax = Integer.MIN_VALUE;

		for (int i = 0; i < MultiSelectionObjects.size(); i++)
		{
			DrawObject o = MultiSelectionObjects.get(i);

			if (o.IsZombie() ||
				(o instanceof CommentField) ||
				(o instanceof Line) ||
				(o instanceof VisualField) ||
				(o instanceof Selection)) // these objects aren't visible or don't have any usable Size info
			{
				continue;
			}

			if (o.Pos.x < xMin)
				xMin = o.Pos.x;
			if (o.Pos.y < yMin)
				yMin = o.Pos.y;

			if ((o.Pos.x + o.Size.x) > xMax)
				xMax = o.Pos.x + o.Size.x;
			if ((o.Pos.y + o.Size.y) > yMax)
				yMax = o.Pos.y + o.Size.y;
		}

		return new Rectangle(xMin, yMin, (xMax - xMin), (yMax - yMin));
	}

	public ArrayList<DrawObject> GetObjectsContainingHormone(String HormoneName)
	{
		ArrayList<DrawObject> ObjectsContainingHormone = new ArrayList<DrawObject>();

		for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
		{
			DrawObject o = i.next();
			if (o.IsNeuron())
			{
				if (((Neuron) o).GetHormoneStrength(HormoneName) != Double.MAX_VALUE)
					ObjectsContainingHormone.add(o);
			}
		}

		return ObjectsContainingHormone;
	}

	public ArrayList<DrawObject> GetSelectionObjects(Point SelectionTopLeftCornerPos, Point SelectionSize)
	{
		ArrayList<DrawObject> SelectionObjects = new ArrayList<DrawObject>();

		int STLx = SelectionTopLeftCornerPos.x;
		int STLy = SelectionTopLeftCornerPos.y;
		int SSx = SelectionSize.x;
		int SSy = SelectionSize.y;

		if (SSx < 0)
		{
			STLx = STLx - -SSx;
			SSx = -SSx;
		}
		if (SSy < 0)
		{
			STLy = STLy - -SSy;
			SSy = -SSy;
		}

		for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
		{
			DrawObject o = i.next();
			if (o.IsLine())
			{
				DrawObject S = ((Line) o).GetStartDrawObject();
				DrawObject E = ((Line) o).GetEndDrawObject();

				if (S.Pos.x >= STLx && S.Pos.x + S.Size.x < STLx + SSx && S.Pos.y >= STLy && S.Pos.y + S.Size.y < STLy + SSy && E.Pos.x >= STLx && E.Pos.x + E.Size.x < STLx + SSx && E.Pos.y >= STLy && E.Pos.y + E.Size.y < STLy + SSy)
				{
					SelectionObjects.add(o); // add Line although it cannot be selected *directly* by user
				}
			}
			else if (o.CanBeSelectedByUser())
			{
				if (o.Pos.x >= STLx && o.Pos.x + o.Size.x < STLx + SSx && o.Pos.y >= STLy && o.Pos.y + o.Size.y < STLy + SSy)
				{
					SelectionObjects.add(o);
				}
			}
		}

		return SelectionObjects;
	}

	public ArrayList<TextField> GetTextFieldsByText(String TextFieldText)
	{
		ArrayList<TextField> TextFields = new ArrayList<TextField>();

		for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
		{
			DrawObject o = i.next();
			if (o.IsTextField())
				if (((TextField) o).GetText().equals(TextFieldText))
					TextFields.add((TextField) o);
		}

		return TextFields;
	}

	public boolean IsMultiSelectionExisting()
	{
		int DrawObjectCount = DrawObjects.size();
		for (int i = 0; i < DrawObjectCount; i++)
			if (DrawObjects.get(i).IsPartOfMultiSelection())
				return true;
		return false;
	}

	public boolean IsObjectAtPoint(Point P)
	{
		for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
		{
			DrawObject o = i.next();
			if (o.ContainsPoint(P))
				return true;
		}

		return false;
	}

	public boolean IsObjectStateValid(DrawObject o)
	{
		if (o == null)
			return false;
		
		if (o.IsZombie())
			return false;
		
		if (o.IsLine())
		{
			if (((Line) o).GetStartDrawObject() == null)
				return false;
			if (((Line) o).GetEndDrawObject() == null)
				return false;
			
			if (((Line) o).GetStartDrawObject().IsZombie())
				return false;
			if (((Line) o).GetEndDrawObject().IsZombie())
				return false;
		}
		
		return true;
	}
	
	public LoadSaveDataEx LoadAdditionalFromFile(String LoadPath)
	{
		return LoadFromFile(LoadPath, true);
	}

	public LoadSaveDataEx LoadAllFromFile(String LoadPath)
	{
		return LoadFromFile(LoadPath, false);
	}

	private LoadSaveDataEx LoadFromFile(String LoadPath, boolean AddToExistingObjects)
	{
		try (DataInputStream is = new DataInputStream(
			new BufferedInputStream(
				new InflaterInputStream(
					// https://www.geeksforgeeks.org/compressing-and-decompressing-files-in-java/ + https://www.tutorialspoint.com/how-to-compress-a-file-in-java
					new FileInputStream(LoadPath)
				)
			)
		)) // https://jenkov.com/tutorials/java-exception-handling/try-with-resources.html
		{
			// verify
			long FileTypeMagicNumberRead = is.readLong();
			if (FileTypeMagicNumberRead != FileTypeMagicNumber)
			{
				is.close();
				return new LoadSaveDataEx(false, "File seems not to have been saved by NCIDE", "", 1.0);
			}

			int FileTypeVersionRead = is.readInt();
			if (FileTypeVersionRead != FileTypeVersion)
			{
				is.close();
				return new LoadSaveDataEx(false, "File version does not match this program's version", "", 1.0);
			}

			// reset
			if (AddToExistingObjects == false)
			{
				// throw away old DrawObjects NOW to free up heap memory,
				// necessary if previously run out of memory (tested)...

				if (DrawObjects != null)
				{
					DrawObjects.clear(); // make sure gc throws away old ArrayList<DrawObject>
					DrawObjects.trimToSize(); // make sure gc throws away old ArrayList<DrawObject>
					DrawObjects = null; // make sure gc throws away old ArrayList<DrawObject>
				}

				System.gc(); // throw away old DrawObjects; not sure if REQUEST to run garbage collector has an effect, but it's not harmful anyway
			}

			// begin reading
			int DrawObjectCount = is.readInt();

			// TODO: the following stuff, maybe unify this somehow,
			// don't copy code all the time...:

			// associates a Neuron with its child DrawObjects:
			NeuronCache NeuronCache = new NeuronCache(); // GetDrawObjectByCompoundId() was too slow if loading CADUs

			Neuron CompoundNeuron = null;
			NeuronInputField CompoundNeuronInputField = null;
			NeuronInputNode CompoundNeuronInputNode = null;
			NeuronOutputNode CompoundNeuronOutputNode = null;

			// associates a SampleAndHold with its child DrawObjects:
			SampleAndHold CompoundSampleAndHold = null;

			// associates a VisualField with its child DrawObjects:
			VisualField CompoundVisualField = null;
			VisualFieldPixel CompoundVisualFieldPixel = null;

			// temporary DrawObject collection, might be added to the main collection or even become it:
			ArrayList<DrawObject> DrawObjectsNew = new ArrayList<DrawObject>(DrawObjectCount);

			// read all DrawObjects saved in file
			for (int i = 0; i < DrawObjectCount; i++)
			{
				String ClassName = Tools.ReadUTF32Bit(is);

				DrawObject n = null; // new DrawObject to assemble, initialize with dummy data, n.StateFromFile(is) loads actual values

				switch (ClassName)
				{
				case "CommentField":
					n = new CommentField(new Point(0, 0), new Point(0, 0));
					n.StateFromFile(is);
					break;
				case "Line":
					n = new Line(null, null);

					int StartDrawObjectIndexInDrawObjects = is.readInt();
					int EndDrawObjectIndexInDrawObjects = is.readInt();

					n.StateFromFile(is); // do AFTER is.readInt() (see above)!

					((Line) n).SetStartDrawObject(DrawObjectsNew.get(StartDrawObjectIndexInDrawObjects));
					((Line) n).SetEndDrawObject(DrawObjectsNew.get(EndDrawObjectIndexInDrawObjects));

					break;
				case "Node":
					n = new Node(new Point(0, 0));
					n.StateFromFile(is);
					break;
				case "Neuron":
					n = new Neuron(new Point(0, 0));
					n.StateFromFile(is); // loads any former CompoundId, which could be in use by already loaded DrawObjects if an additional file is being loaded as SubSystem
					NeuronCache.AddNeuron((Neuron) n);
					break;
				case "NeuronInputField":
					n = new NeuronInputField(new Point(0, 0));
					n.StateFromFile(is);
					CompoundNeuronInputField = (NeuronInputField) n;
					CompoundNeuron = NeuronCache.GetNeuronByCompoundId(n.GetCompoundId());
					if (CompoundNeuron == null) // fallback
						CompoundNeuron = (Neuron) GetDrawObjectByCompoundId(new Neuron(new Point(0, 0)), n.GetCompoundId(), DrawObjectsNew); // use the CompoundId saved in file
					if (CompoundNeuron != null)
					{
						if (CompoundNeuronInputField != null && CompoundNeuronInputNode != null)
						{
							CompoundNeuron.AddInputFieldAndNode(CompoundNeuronInputField, CompoundNeuronInputNode);
							CompoundNeuronInputField = null;
							CompoundNeuronInputNode = null;
						}
					}
					break;
				case "NeuronInputNode":
					n = new NeuronInputNode(new Point(0, 0));
					n.StateFromFile(is);
					CompoundNeuronInputNode = (NeuronInputNode) n;
					CompoundNeuron = NeuronCache.GetNeuronByCompoundId(n.GetCompoundId());
					if (CompoundNeuron == null) // fallback
						CompoundNeuron = (Neuron) GetDrawObjectByCompoundId(new Neuron(new Point(0, 0)), n.GetCompoundId(), DrawObjectsNew); // use the CompoundId saved in file
					if (CompoundNeuron != null)
					{
						if (CompoundNeuronInputField != null && CompoundNeuronInputNode != null)
						{
							CompoundNeuron.AddInputFieldAndNode(CompoundNeuronInputField, CompoundNeuronInputNode);
							CompoundNeuronInputField = null;
							CompoundNeuronInputNode = null;
						}
					}
					break;
				case "NeuronOutputNode":
					n = new NeuronOutputNode(new Point(0, 0));
					n.StateFromFile(is);
					CompoundNeuronOutputNode = (NeuronOutputNode) n;
					CompoundNeuron = NeuronCache.GetNeuronByCompoundId(n.GetCompoundId());
					if (CompoundNeuron == null) // fallback
						CompoundNeuron = (Neuron) GetDrawObjectByCompoundId(new Neuron(new Point(0, 0)), n.GetCompoundId(), DrawObjectsNew); // use the CompoundId saved in file
					if (CompoundNeuron != null)
						CompoundNeuron.AddOutputNode(CompoundNeuronOutputNode);
					break;
				case "SampleAndHold":
					n = new SampleAndHold(new Point(0, 0));
					n.StateFromFile(is);
					CompoundSampleAndHold = (SampleAndHold) n;
					break;
				case "SampleAndHoldInputNode":
					n = new SampleAndHoldInputNode(new Point(0, 0));
					n.StateFromFile(is);
					if (CompoundSampleAndHold != null) // parent object (SampleAndHold) ALWAYS right before its input and output nodes, in memory and in file
					{
						CompoundSampleAndHold.AddInputNode((SampleAndHoldInputNode) n);
					}
					break;
				case "SampleAndHoldOutputNode":
					n = new SampleAndHoldOutputNode(new Point(0, 0));
					n.StateFromFile(is);
					if (CompoundSampleAndHold != null) // parent object (SampleAndHold) ALWAYS right before its input and output nodes, in memory and in file
					{
						CompoundSampleAndHold.AddOutputNode((SampleAndHoldOutputNode) n);
					}
					break;
				case "TextField":
					n = new TextField(new Point(0, 0));
					n.StateFromFile(is);
					break;
				case "VisualField":
					n = new VisualField(new Point(0, 0));
					n.StateFromFile(is);
					CompoundVisualField = (VisualField) n;
					break;
				case "VisualFieldPixel":
					n = new VisualFieldPixel(new Point(0, 0));
					n.StateFromFile(is);
					CompoundVisualFieldPixel = (VisualFieldPixel) n;
					if (CompoundVisualField != null) // parent object (VisualField) ALWAYS right before its pixels, in memory and in file
					{
						CompoundVisualField.AddVisualFieldPixel(CompoundVisualFieldPixel);
					}
					break;
				}

				if (IsObjectStateValid(n))
				{
					DrawObjectsNew.add(n); // finally add new DrawObject to temporary collection
				}

				if ((i % 10000) == 0)
				{
					if (!(Tools.CheckHeapSize()))
					{
						// ****************************************************
						is.close();
						return new LoadSaveDataEx(false, "not enough memory left (too many objects) - you might not be able to add further objects", "", 1.0);
						// ****************************************************
					}
				}
			}

			// read tag string, contains the "IO program" to be worked up when running simulation
			String AdditionalData = Tools.ReadUTF32Bit(is);

			is.close();

			// adapt CompoundIds
			if (AddToExistingObjects)
			{
				// determine greatest CompoundId value currently in use by the
				// FORMERLY loaded file(s) (the following does only make sense
				// when ADDING DrawObjectsNew as SubSystem):

				long CompoundIdLoadedMax = -1;

				for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
				{
					DrawObject o = i.next();
					if (o.GetCompoundId() != DrawObject.ID_UNSET)
						if (o.GetCompoundId() > CompoundIdLoadedMax)
							CompoundIdLoadedMax = o.GetCompoundId();
				}

				// make sure NEWLY added objects have a greater CompoundId:

				long CompoundIdShift = +(CompoundIdLoadedMax + 1); // add 1!
				long CompoundIdAssignedMax = -1;

				for (Iterator<DrawObject> i = DrawObjectsNew.iterator(); i.hasNext();)
				{
					DrawObject o = i.next();
					if (o.GetCompoundId() != DrawObject.ID_UNSET)
					{
						long CompoundIdAdapted = o.GetCompoundId() + CompoundIdShift;
						o.SetCompoundId(CompoundIdAdapted);
						if (CompoundIdAdapted > CompoundIdAssignedMax)
							CompoundIdAssignedMax = CompoundIdAdapted;
					}
				}

				// also make sure newly CREATED (using the 'n' key) DrawObjects have an even greater CompoundId:

				DrawObject.SetMinimalCompoundId(CompoundIdAssignedMax + 1);
			}

			// finally set DrawObjectsNew in use
			if (AddToExistingObjects)
			{
				DrawObjects.addAll(DrawObjectsNew); // add the temporary DrawObjects collection to the main collection
			}
			else
			{
				if (DrawObjects != null)
				{
					DrawObjects.clear(); // make sure gc throws away old ArrayList<DrawObject>
					DrawObjects.trimToSize(); // make sure gc throws away old ArrayList<DrawObject>
					DrawObjects = null; // make sure gc throws away old ArrayList<DrawObject>
				}

				DrawObjects = DrawObjectsNew; // make the temporary DrawObjects collection the main collection of the whole program

				System.gc(); // throw away old DrawObjects; not sure if REQUEST to run garbage collector has an effect, but it's not harmful anyway
			}

			return new LoadSaveDataEx(AdditionalData);
		}
		catch (Exception e)
		{
			System.out.println("error in ObjectStorage.LoadFromFile(): " + e.getMessage());

			if (DrawObjects == null)
			{
				// create any new object collection or the system gets completely screwed up (tested):
				DrawObjects = new ArrayList<DrawObject>();
			}

			return new LoadSaveDataEx(false, e.getMessage(), "", 1.0);
		}
	}

	public void LoseExcitementForAllObjects()
	{
		for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
		{
			DrawObject o = i.next();
			o.LoseExcitementAndAddExcitementForNextTick();
		}
	}

	public void MoveAll(Point MoveAmount)
	{
		for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
		{
			DrawObject o = i.next();
			if (o.CanBeMovedByUser() && !(o.HasParent()))
			{
				o.Move(MoveAmount);
			}
		}
	}

	public void MoveMultiSelection(Point MoveAmount)
	{
		for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
		{
			DrawObject o = i.next();
			if (o.IsPartOfMultiSelection())
			{
				if (o.CanBeMovedByUser() && !(o.HasParent()))
				{
					o.Move(MoveAmount);
				}
			}
		}
	}

	public void ReplaceHormoneNameForAll(String HormoneNameIn, String HormoneNameOut)
	{
		for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
		{
			DrawObject o = i.next();
			if (o.IsNeuron())
			{
				((Neuron) o).ReplaceHormoneName(HormoneNameIn, HormoneNameOut);
			}
		}
	}

	public void ResetStateForAllObjects()
	{
		for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
		{
			DrawObject o = i.next();
			o.ResetState();
		}
	}

	public void ResetUpdateHormoneStrengthForAll()
	{
		HormonesAlreadyUpdated.clear();
	}

	public LoadSaveDataEx SaveAllToFile(String SavePath, LoadSaveDataEx LoadSaveDataEx)
	{
		return SaveToFile(SavePath, false, DrawObjects, LoadSaveDataEx);
	}

	public LoadSaveDataEx SaveEmptyFile(String EmptyFilePath)
	{ // returns error description or "" in case of no error

		ArrayList<DrawObject> EmptyDrawObjects = new ArrayList<DrawObject>();

		EmptyDrawObjects.clear(); // just to make sure there's nothing in it

		return SaveToFile(EmptyFilePath, false, EmptyDrawObjects, new LoadSaveDataEx(true, "", "", 1.0));
	}

	public LoadSaveDataEx SaveMultiSelectionToFile(String SavePath, LoadSaveDataEx LoadSaveDataEx)
	{
		return SaveToFile(SavePath, true, DrawObjects, LoadSaveDataEx);
	}

	private LoadSaveDataEx SaveToFile(String SavePath, boolean SaveMultiSelectionOnly, ArrayList<DrawObject> DrawObjects, LoadSaveDataEx LoadSaveDataEx)
	{
		try (DataOutputStream os = new DataOutputStream(
			new BufferedOutputStream(
				new DeflaterOutputStream(
					// ^ bless it, just this one code line reduced 600 MB to 22 MB in test, from https://www.tutorialspoint.com/how-to-compress-a-file-in-java
					new FileOutputStream(SavePath)
				)
			)
		)) // https://jenkov.com/tutorials/java-exception-handling/try-with-resources.html
		{
			int DrawObjectCount;

			if (SaveMultiSelectionOnly)
			{
				DrawObjectCount = 0;
				for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
					if (i.next().IsPartOfMultiSelection)
						DrawObjectCount++;
			}
			else
			{
				DrawObjectCount = DrawObjects.size();
			}

			os.writeLong(FileTypeMagicNumber);
			os.writeInt(FileTypeVersion);
			os.writeInt(DrawObjectCount);

			int DrawObjectsSavedCount = 0;
			HashMap<DrawObject, Integer> DrawObjectsSaved = new HashMap<DrawObject, Integer>();

			for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
			{
				DrawObject o = i.next();
				if (IsObjectStateValid(o))
				{
					if (!(SaveMultiSelectionOnly) || o.IsPartOfMultiSelection)
					{
						String ClassName = o.getClass().getSimpleName();
						Tools.WriteUTF32Bit(os, ClassName);
	
						if (o.IsLine())
						{
							// we search, using random access, a DrawObject we've already saved,
							// to get its index (how many objects have been saved to file before?);
							// therefore we use a HashMap, which is searched very fast
							// (looping through DrawObjects was much too slow):
	
							int StartDrawObjectIndexInDrawObjects = DrawObjectsSaved.get(((Line) o).GetStartDrawObject()) - 1;
							int EndDrawObjectIndexInDrawObjects = DrawObjectsSaved.get(((Line) o).GetEndDrawObject()) - 1;
	
							os.writeInt(StartDrawObjectIndexInDrawObjects);
							os.writeInt(EndDrawObjectIndexInDrawObjects);
						}
	
						o.StateToFile(os);
	
						DrawObjectsSavedCount++;
						DrawObjectsSaved.put(o, DrawObjectsSavedCount);
					}
				}
			}

			Tools.WriteUTF32Bit(os, LoadSaveDataEx.StateToFileString());
			os.close();

			return new LoadSaveDataEx(true, "", "", 1.0);
		}
		catch (Exception e)
		{
			System.out.println("error in ObjectStorage.SaveToFile(): " + e.getMessage());

			return new LoadSaveDataEx(false, e.getMessage(), "", 1.0);
		}
	}

	public void SetPartOfMultiSelection(int DrawObjectIndex)
	{
		if (DrawObjectIndex >= 0 && DrawObjectIndex < DrawObjects.size())
			DrawObjects.get(DrawObjectIndex).SetPartOfMultiSelection(true);
		else
			System.out.println("error in ObjectStorage.SetPartOfMultiSelection(): passed value not useful!");
	}

	public void ToggleNeuronInputFieldByIndex(ArrayList<DrawObject> Objects, int InputFieldIndex)
	{
		for (Iterator<DrawObject> i = Objects.iterator(); i.hasNext();)
		{
			DrawObject o = i.next();
			if (o.IsNeuron())
			{
				((Neuron) o).ToggleInputFieldByIndex(InputFieldIndex);
			}
		}
	}

	public void TransferActivatedTextFieldExcitement()
	{
		// NOTE: DO NOT USE TextField.AddExcitementForNextTick(), because then
		// TextFields will be activated in the NEXT tick. This leads to another
		// call of this procedure, which would call TextField.AddExcitementForNextTick()
		// AGAIN. As a result, TextFields would stay activated durably (tested).
		// Instead, access the excitement values DIRECTLY - "transfer" the excitement
		// state of one TextField to all others with the same text.

		ArrayList<String> ActivatedTextFieldsText = new ArrayList<String>(); // we don't use NESTED for-all-DrawObjects-loops, of course
		for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
		{
			DrawObject o = i.next();
			if (o.IsTextField()) // additional check (actually surplus, but just to make sure), ObjectStorage.GetActivatedObjects([...]) should have returned TextFields only
			{
				if (o.DoesActivate(o.ExcitementAddForNextTick))
				{
					String TextFieldText = ((TextField) o).GetText();

					if (!(ActivatedTextFieldsText.contains(TextFieldText))) // .contains() checks String CONTENT, not reference (tested), so this is effective!
						ActivatedTextFieldsText.add(TextFieldText);
				}
			}
		}

		// System.out.println("exciting " + ActivatedTextFieldsText.size() + " TextFields");

		for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
		{
			DrawObject o = i.next();
			if (o.IsTextField()) // additional check (actually surplus, but just to make sure), ObjectStorage.GetActivatedObjects([...]) should have returned TextFields only
			{
				for (int t = 0; t < ActivatedTextFieldsText.size(); t++) // loop through this only if we have a TextField
				{
					if (((TextField) o).GetText().equals(ActivatedTextFieldsText.get(t)))
					{
						o.ExcitementAddForNextTick = 1.0;
					}
				}
			}
		}
	}

	public DrawObject TrySelectDrawObject(Point MousePosPoint)
	{
		// prefer (in z-order) objects the user usually wants to select
		for (int i = DrawObjects.size() - 1; i >= 0; i--)
		{
			// loop in reverse order because when drawing, the DrawObjects with highest
			// index are drawn last, so that they are visible to the user on top -
			// and the user expects to select the object he sees on top

			DrawObject o = DrawObjects.get(i);
			if (!(o.IsLine()))
			{
				if (o.ContainsPoint(MousePosPoint))
					return o;
			}
		}

		// if nothing clearly selected, check if any Line is near
		for (int i = DrawObjects.size() - 1; i >= 0; i--)
		{
			// loop in reverse order because when drawing, the DrawObjects with highest
			// index are drawn last, so that they are visible to the user on top -
			// and the user expects to select the object he sees on top

			DrawObject o = DrawObjects.get(i);
			if (o.IsLine())
			{
				double Distance = Math.abs(((Line) o).GetDistanceToPoint(MousePosPoint));
				if (Distance < 5)
				{
					return o;
				}
			}
		}

		return null;
	}

	public DrawObject TrySelectNearObject(Point MousePosPoint)
	{
		double DistanceMin = Double.MAX_VALUE;
		DrawObject DistanceMinDrawObject = null;

		for (int i = DrawObjects.size() - 1; i >= 0; i--)
		{
			// loop in reverse order because when drawing, the DrawObjects with highest
			// index are drawn last, so that they are visible to the user on top -
			// and the user expects to select the object he sees on top

			DrawObject o = DrawObjects.get(i);
			if (o.IsLine())
			{
				double Distance = Math.abs(((Line) o).GetDistanceToPoint(MousePosPoint));
				if (Distance < 25 && Distance < (DistanceMin - 1.0)) // we subtract 1.0 to (effectively) avoid flickering in case there are two or more Lines above each other, especially when the Lines of CommentFields overlap
				{
					DistanceMin = Distance;
					DistanceMinDrawObject = o;
				}
			}
			else
			{
				if (o.ContainsPoint(MousePosPoint))
					return o; // return instantly, it's the best object for sure
			}
		}

		return DistanceMinDrawObject;
	}

	public void UnHighlightAllObjects()
	{
		for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
		{
			DrawObject o = i.next();
			o.SetHighlighted(false);
		}
	}

	public void UnSetPartOfMultiSelectionForAllObjects()
	{
		for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
		{
			DrawObject o = i.next();
			o.SetPartOfMultiSelection(false);
		}
	}

	public void UpdateHormoneStrengthForAll(String HormoneName, double StrengthNew)
	{
		for (int n = 0; n < HormonesAlreadyUpdated.size(); n++) // we loop through (very few) Hormone NAMES, NOT through (possibly very many) DrawObjects using this Hormone name!
			if (HormonesAlreadyUpdated.get(n).equals(HormoneName))
				return; // if multiple StrengthNews for the same Hormone name are passed (what is not useful anyway), all except the first Strength are ignored (this behavior would also show up WITHOUT this speed-up!)

		HormonesAlreadyUpdated.add(HormoneName);

		for (Iterator<DrawObject> i = DrawObjects.iterator(); i.hasNext();)
		{
			DrawObject o = i.next();
			if (o.IsNeuron())
			{
				((Neuron) o).SetHormoneStrength(HormoneName, StrengthNew);
			}
		}
		return;
	}
}
